package cn.edu.buaa.sei.jdat;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.HashSet;
import java.util.List;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.bcel.Constants;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.ConstantClass;
import org.apache.bcel.classfile.ConstantString;
import org.apache.bcel.classfile.ConstantUtf8;
import org.apache.bcel.classfile.DescendingVisitor;
import org.apache.bcel.classfile.EmptyVisitor;
import org.apache.bcel.classfile.FieldOrMethod;
import org.apache.bcel.classfile.JavaClass;

import cn.edu.buaa.sei.jdat.exception.InvalidURIException;
import cn.edu.buaa.sei.jdat.graph.DependencyGraph;
import cn.edu.buaa.sei.jdat.io.JarLoader;
import cn.edu.buaa.sei.jdat.model.Field;
import cn.edu.buaa.sei.jdat.model.IModifier;
import cn.edu.buaa.sei.jdat.model.Interface;
import cn.edu.buaa.sei.jdat.model.Jar;
import cn.edu.buaa.sei.jdat.model.Member;
import cn.edu.buaa.sei.jdat.model.Method;
import cn.edu.buaa.sei.jdat.model.Package;
import cn.edu.buaa.sei.jdat.model.Type;
import cn.edu.buaa.sei.jdat.model.Class;

//
//
//  Generated by StarUML(tm) Java Add-In
//
//  @ Project : Jar Dependency Analysis Toolkit
//  @ File Name : JarController.java
//  @ Date : 2012/1/13
//  @ Author : 
//
//

public abstract class JarController {
	
	public static interface ProgressMonitor {
		void updateProgress(double progress);
		void finished();
	}
	
	private JarLoader loader = new JarLoader();

	private final JarCollection collection;
	private final List<Jar.StateChangeListener> stateChangeListeners;
	private final UnresolvedTable unresolvedTable;
	private final ResolvedTable resolvedTable;
	
	/**
	 * This table records unresolved names of types. With each type name, there is a list of 
	 * types that refer to the type. When the type name is be transformed to instance of type, 
	 * this table will notify all types that refer to this type to update their references 
	 * list.
	 * @author Alven
	 *
	 */
	public static class UnresolvedTable {
		
		private Hashtable<String, ArrayList<Type>> table = new Hashtable<String, ArrayList<Type>>();
		
		public void addItem(String refName, Type type) {
			if (table.containsKey(refName)) {
				table.get(refName).add(type);
			} else {
				ArrayList<Type> array = new ArrayList<Type>();
				array.add(type);
				table.put(refName, array);
			}
		}
		
		public void removeItem(String refName, Type type) {
			if (table.containsKey(refName)) {
				ArrayList<Type> types = table.get(refName);
				types.remove(type);
				if (types.size() == 0)
					table.remove(refName);
			}
		}
		
		public void removeName(String refName) {
			table.remove(refName);
		}
		
		public List<Type> getTypes(String refName) {
			return table.get(refName);
		}
	}
	
	/**
	 * This table records resolved names of types. With each type name, there is a list of types
	 * that their signatures is same with the name. Namely, the table records types with the same
	 * names. The references to a type will only point to the first instance in same-name-types.
	 * When one type is removed, it's available with this table to check whether there 
	 * is another type with the same name, and if there is, all references to the removed type 
	 * will point to this same-name-type, and otherwise, the name will be added in unresolved table.
	 * @author Alven
	 *
	 */
	public static class ResolvedTable {
		private Hashtable<String, ArrayList<Type>> table = new Hashtable<String, ArrayList<Type>>();
		
		public void addType(Type type) {
			String name = type.getSignature();
			if (table.containsKey(name)) {
				table.get(name).add(type);
			} else {
				ArrayList<Type> array = new ArrayList<Type>();
				array.add(type);
				table.put(name, array);
			}
		}
		
		public void removeType(Type type) {
			String name = type.getSignature();
			if (table.containsKey(name)) {
				ArrayList<Type> array = table.get(name);
				array.remove(type);
				if (array.size() == 0)
					table.remove(name);
			}
		}
		
		public Type getType(String name) {
			return table.containsKey(name) ? table.get(name).get(0) : null;
		}
	}
	
	public JarController() {
		collection = new JarCollection();
		stateChangeListeners = new ArrayList<Jar.StateChangeListener>();
		unresolvedTable = new UnresolvedTable();
		resolvedTable = new ResolvedTable();
	}
	
	public JarLoader getLoader() {
		return loader;
	}
	
	abstract protected void importJarStarted(Jar jar);
	abstract protected void importJarFinished(Jar jar);
	abstract protected void removeJarStarted(Jar jar);
	abstract protected void removeJarFinished(Jar jar);
	
	private static boolean validate(String url) {
		//TODO
		
		return true;
	}
	
	public void importJar(String uri) throws InvalidURIException, IOException {
		this.importJar(uri, null);
	}
	
	public void importJar(String uri, ProgressMonitor monitor) throws InvalidURIException, IOException {
		if (!validate(uri)) {
			throw new InvalidURIException(uri);
		}
		Jar jar = new Jar(uri);
		
		// realize listener
		Jar.StateChangeListener listener = new Jar.StateChangeListener() {
			
			@Override
			public void stateChange(Jar jar, int state) {
				// TODO Auto-generated method stub
				jarStateChanged(jar, state);
			}
		};
		jar.setListener(listener);
		stateChangeListeners.add(listener);
		collection.add(jar);
		importJarStarted(jar);
		jar.setState(Jar.INITIALIZED);
		analyze(jar, monitor);
		importJarFinished(jar);
	}
	
	abstract protected void jarStateChanged(Jar jar, int state);
	
	public void removeJar(Jar jar, ProgressMonitor monitor) {
		removeJarStarted(jar);
		deleteDependencies(jar, monitor);
		collection.remove(jar);
		removeJarFinished(jar);
	}
	
	public void removeJar(Jar jar) {
		removeJar(jar, null);
	}
	
	public Jar getJar(int index) {
		return collection.get(index);
	}
	
	public int indexOfJar(Jar jar) {
		return collection.indexOf(jar);
	}
	
	public Jar[] getJars(int[] indices) {
		int size = indices.length;
		Jar[] jars = new Jar[size];
		for (int i = 0; i < size; i++) {
			jars[i] = collection.get(indices[i]);
		}
		return jars;
	}
	
	public DependencyGraph generateGraph(Jar[] jars) {
		// TODO
		return null;
	}
	
	public int getJarCount() {
		return collection.size();
	}
	
	private void analyze(Jar jar, ProgressMonitor monitor) throws IOException {
		jar.setState(Jar.LOADING);
		File jarfile = loader.load(jar.getURI());
		jar.setName(jarfile.getName());
		analyze(jar, jarfile, monitor);
		jar.setState(Jar.READY);
	}
	
	private synchronized void analyze(Jar jar, File file, ProgressMonitor monitor) throws IOException {
		final JarFile jarFile = new JarFile(file);
		final ArrayList<JarEntry> classEntries = new ArrayList<JarEntry>();
		
		// enumerate classes in jar
		Enumeration<JarEntry> it = jarFile.entries();
		while (it.hasMoreElements()) {
			JarEntry jarEntry = it.nextElement();
			if (jarEntry.getName().endsWith(".class")) {
				classEntries.add(jarEntry);
			}
		}
		
		jar.setState(Jar.PARSING);
		// process class
		for (int i = 0; i < classEntries.size(); i++) {
			JarEntry jarEntry = classEntries.get(i);
			
			// parse class
			InputStream is = jarFile.getInputStream(jarEntry);
			ClassParser parser = new ClassParser(is, jarEntry.getName());
			JavaClass javaClass = parser.parse();
			
			// create owner package
			String packageName = javaClass.getPackageName();
			Package p = jar.getPackage(packageName);
			if (p == null) {
				p = new Package();
				p.setName(packageName);
				jar.addPackage(p);
			}
			
			// create type
			Type type = javaClass.isInterface() ? new Interface() : new Class();
			p.addType(type); // add type to package
			type.setOwner(p);
			String name = javaClass.getClassName();
			if (name.contains(".")) {
				name = name.substring(name.lastIndexOf('.') + 1);
			}
			type.setName(name);
			org.apache.bcel.classfile.Field[] fields = javaClass.getFields();
			org.apache.bcel.classfile.Method[] methods = javaClass.getMethods();
			
			// add field to type
			for (org.apache.bcel.classfile.Field field : fields) {
				Field f = new Field();
				f.setName(field.getName());
				f.setType(Type.createUnresolvedType(field.getType().toString()));
				f.setOwner(type);
				type.addMember(f);
				setModifier(f, field);
			}
			
			// add methods to type
			for (org.apache.bcel.classfile.Method method : methods) {
				Method m = new Method();
				m.setName(method.getName());
				m.setReturnType(Type.createUnresolvedType(method.getReturnType().toString()));
				for (org.apache.bcel.generic.Type at : method.getArgumentTypes()) {
					m.addArgument(Type.createUnresolvedType(at.toString()));
				}
				m.setOwner(type);
				type.addMember(m);
				setModifier(m, method);
			}
			
			// set type property
			type.setAbstract(javaClass.isAbstract());
			type.setStatic(javaClass.isStatic());
			type.setFinal(javaClass.isFinal());
			int modifiers = javaClass.getModifiers();
			if ((modifiers & Constants.ACC_PUBLIC) != 0) {
				type.setAccessModifiers(IModifier.PUBLIC);
			} else if ((modifiers & Constants.ACC_PROTECTED) != 0) {
				type.setAccessModifiers(IModifier.PROTECTED);
			} else if ((modifiers & Constants.ACC_PRIVATE) != 0) {
				type.setAccessModifiers(IModifier.PRIVATE);
			} else {
				type.setAccessModifiers(IModifier.DEFAULT);
			}
			
			// get all references of this type
			String[] refs = parseReference(javaClass);
			for (String ref : refs) {
				Type t = resolvedTable.getType(ref);
				if (t != null) {
					t.addInverseReference(type);
					type.addReference(t);
				} else {
					// update UnresolvedTable
					unresolvedTable.addItem(ref, type);
				}
				type.addReferenceName(ref);
			}
			
			// fill super-types into type
			type.addSupertypeName(javaClass.getSuperclassName());
			if (!javaClass.isInterface()) {
				((Class)type).setSuperclassName(javaClass.getSuperclassName());
			}
			String[] interfaceNames = javaClass.getInterfaceNames();
			for (String interfaceName : interfaceNames) {
				type.addSupertypeName(interfaceName);
			}

			// update ResolvedTable
			resolvedTable.addType(type);
			
			// update UnresolvedTable
			List<Type> unresolvedRefType = unresolvedTable.getTypes(type.getSignature());
			if (unresolvedRefType != null) {
				for (Type t : unresolvedRefType) {
					t.addReference(type);
					type.addInverseReference(t);
				};
				unresolvedTable.removeName(type.getSignature());
			}
			
			// update monitor
			if (monitor != null) monitor.updateProgress((i + 1) / (double) classEntries.size());
		}
		
		if (monitor != null) monitor.finished();
	}
	
	private void setModifier(Member member, FieldOrMethod fm) {
		member.setAbstract(fm.isAbstract());
		member.setStatic(fm.isStatic());
		member.setFinal(fm.isFinal());
		int modifiers = fm.getModifiers();
		if ((modifiers & Constants.ACC_PUBLIC) != 0) {
			member.setAccessModifiers(IModifier.PUBLIC);
		} else if ((modifiers & Constants.ACC_PROTECTED) != 0) {
			member.setAccessModifiers(IModifier.PROTECTED);
		} else if ((modifiers & Constants.ACC_PRIVATE) != 0) {
			member.setAccessModifiers(IModifier.PRIVATE);
		} else {
			member.setAccessModifiers(IModifier.DEFAULT);
		}
	}
	
	private String[] parseReference(final JavaClass javaClass) {
		class DependencyVisitor extends EmptyVisitor {
			
			final public ArrayList<String> strings = new ArrayList<String>();
			final public HashSet<String> reference = new HashSet<String>();
			
			public void visitConstantClass(ConstantClass cls) {
				String sCls = cls.getBytes(javaClass.getConstantPool());
				if (sCls.indexOf("/") != -1) {
					sCls = sCls.replace('/', '.');
					sCls = this.cleanClass(sCls);
					if (!reference.contains(sCls) && !sCls.equals(javaClass.getClassName())) {
						reference.add(sCls);
					}
				}
			}

			public void visitConstantUtf8(ConstantUtf8 utf) {
				String utfString = utf.toString().substring(utf.toString().indexOf('"') + 1,utf.toString().lastIndexOf('"'));
				if (isValidJavaClass(utfString)) {
					if (!this.strings.contains(utfString)) {
						String[] classes = this.separateClasses(utfString);
						for (int i = 0; i < classes.length; i++) {
							if (classes[i] != null) {
								String cls = classes[i];
								// String packageName = this.stripClassName(cls);
								cls = cls.replace('/', '.');
								cls = this.cleanClass(cls);
								if (!reference.contains(cls) && !cls.equals(javaClass.getClassName())) {
									reference.add(cls);
								}
							}
						}
					}
				}

			}

			public void visitConstantString(ConstantString str) {
				this.strings.add(str.getBytes(javaClass.getConstantPool()).toString());
			}
			
			private boolean isValidJavaClass(String cls) {
				if (cls.indexOf("/") == -1) {
					return false;
				}
				if ( (!cls.startsWith("(")) && (!cls.startsWith("L")) ) {
					return false;
				}

				if ( (!cls.endsWith("V")) && (!cls.endsWith(";")) && (!cls.endsWith(")")) ) {
					return false;
				}

				return true;
			}

			private String[] separateClasses(String utfString) {
				StringTokenizer tokenizer = new StringTokenizer(utfString, ";");
				String classes[] = new String[tokenizer.countTokens()];
				int i = 0;
				while (tokenizer.hasMoreTokens()) {
					String cls = tokenizer.nextToken();
					if (cls.indexOf('/') != -1) {
						classes[i] = cls;
						i++;
					}
				}
				return classes;
			}

			private String cleanClass(String cls) {
				if (cls.startsWith("L")) return cls.substring(1);
				else if (cls.contains(".")) { // not in default package
					int index = cls.substring(0, cls.lastIndexOf(".")).lastIndexOf('L');
					if (index != -1) {
						return cls.substring(index + 1);
					}
				}
				return cls;
			}
			
		}
		
		DependencyVisitor refVisitor = new DependencyVisitor();
		DescendingVisitor descendingVisitor = new DescendingVisitor(javaClass, refVisitor);
		javaClass.accept(descendingVisitor);
		return refVisitor.reference.toArray(new String[0]);
	}
	
	private synchronized void deleteDependencies(Jar jar, ProgressMonitor monitor) {
		int allcount = 0;
		for (Package pkg : jar.getPackages()) {
			allcount += pkg.getTypes().size();
		}
		
		int count = 0;
		
		for (Package pkg : jar.getPackages()) {
			for (Type type : pkg.getTypes()) {
				// remove type in resolved table first, 
				// in order to get next type with same name
				resolvedTable.removeType(type);
				Type succ = resolvedTable.getType(type.getSignature());
				for (Type t : type.getReferences()) {
					t.removeInverseReference(type);
				}
				for (String unres : type.getUnresolvedReferences()) {
					// Update unresolved table.
					unresolvedTable.removeItem(unres, type);
				}
				for (Type t : type.getInverseReferences()) {
					t.removeReference(type);
					if (succ != null) {
						// move the dependencies to successor.
						succ.addInverseReference(t);
						t.addReference(succ);
					} else {
						// Update unresolved table.
						unresolvedTable.addItem(type.getSignature(), t);
					}
				}
				
				count++;
				monitor.updateProgress((double) count / allcount);
			}
			for (Package p : pkg.getReferences()) {
				p.removeInverseReference(pkg);
			}
			for (Package p : pkg.getInverseReferences()) {
				p.removeReference(pkg);
			}
		}
		for (Jar j : jar.getReferences()) {
			j.removeInverseReference(jar);
		}
		for (Jar j : jar.getInverseReferences()) {
			j.removeReference(jar);
		}
		
		monitor.finished();
	}
	
	public static void main(String[] args) {
		JarController c = new JarController() {

			@Override
			protected void importJarStarted(Jar jar) {
				// TODO Auto-generated method stub
				
			}

			@Override
			protected void removeJarFinished(Jar jar) {
				// TODO Auto-generated method stub
				
			}

			@Override
			protected void importJarFinished(Jar jar) {
				// TODO Auto-generated method stub
				
			}

			@Override
			protected void removeJarStarted(Jar jar) {
				// TODO Auto-generated method stub
				
			}

			@Override
			protected void jarStateChanged(Jar jar, int state) {
				// TODO Auto-generated method stub
				
			}
			
		};
		Jar jar = new Jar("url");
		try {
			c.analyze(jar, new File("lib/bcel-5.2.jar"), null);
			jar.toString();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
}
